import type React from "react";
import { type RefObject, useCallback, useEffect, useRef, useState } from "react";
import { VariableSizeList } from "react-window";

type ExtendedData = Readonly<{
  id: string;
  isLeaf: boolean;
  name: string;
  nestingLevel: number;
  padding: number;
  path: string[];
}>;

export interface ExtendedDataWithToggle extends ExtendedData {
  toggle: (id: string) => void;
}

export interface RowProps {
  data: ExtendedData;
  style: any;
}

export interface RowItem {
  children?: RowItem[];
  label: string;
  depth: number;
  path: string[];
  isOpen: boolean;
}

type transformationCallback = ({
  node,
  nestingLevel,
  isFiltering,
  isLeaf,
  childCount,
  isOpen,
}: {
  node: RowItem;
  nestingLevel: number;
  isOpen: boolean;
  isFiltering: boolean;
  isLeaf: boolean;
  childCount: number | undefined;
}) => ExtendedData;

const countChildNodes = (item: RowItem[]) => {
  let counter = 0;
  let index = item.length;

  while (index--) {
    counter++;
    const children = item[index].children;

    if (children) counter += countChildNodes(children);
  }
  return counter;
};

const blankItem = (path: string[], depth: number): RowItem => ({ label: "", depth, path, isOpen: true });
let heightAccumulator: { [key: string]: number } = {};

const TreeStructure = ({
  items,
  rowComponent,
  flatten,
  rowHeight,
  maxHeightPercentage,
  minWidth,
  maxWidth,
  transformationCallback,
  defaultExpanded,
  isEditable,
}: {
  items: any[];
  isEditable?: boolean;
  rowComponent: React.FC<any>;
  flatten: boolean;
  rowHeight: number;
  maxHeightPercentage: number;
  minWidth: number;
  maxWidth: number;
  defaultExpanded: boolean;
  transformationCallback: transformationCallback;
}) => {
  const browserHeight = document.body.clientHeight;

  const [data, setData] = useState<ExtendedData[]>();
  const [openNodes, setOpenNodes] = useState<{ [key: string]: number }>({});
  const [containerHeight, setContainerHeight] = useState(0);
  const [width, setWidth] = useState(minWidth);
  const listRef = useRef<RefObject<HTMLDivElement> | any>();
  const containerRef = useRef<RefObject<HTMLDivElement> | any>();
  const scrollableElement = containerRef.current?.firstChild;

  if (scrollableElement) scrollableElement.style.overflowX = "hidden";

  const rowHeightCalc = (index: number): number => {
    return heightAccumulator[`${index}`] || rowHeight;
  };

  const rowHeightReCalcAll = () => {
    heightAccumulator = {};
    listRef.current.resetAfterIndex(0);
  };

  const containerHeightCalc = () => {
    listRef.current.resetAfterIndex(0);

    const visibleHeight = listRef.current?._outerRef.firstChild?.offsetHeight;
    const maxHeight = maxHeightPercentage * 0.01 * browserHeight;

    return visibleHeight > maxHeight ? maxHeight : visibleHeight;
  };

  const updateHeight = () => {
    setContainerHeight(containerHeightCalc());
  };

  const toggle = (id: string) => {
    const toggleItem = defaultExpanded
      ? {
          [id]: openNodes[id] !== 2 ? 2 : 1,
        }
      : {
          [id]: openNodes[id] !== 1 ? 1 : 2,
        };

    setOpenNodes({ ...openNodes, ...toggleItem });
    setData(recursiveTreeWalker({ items, toggleItem }));
    setContainerHeight(maxHeightPercentage * 0.01 * browserHeight);
    rowHeightReCalcAll();
  };

  const addInside = (id?: string) => {
    if (!isEditable) return;

    if (id) {
      setData(recursiveTreeWalker({ items, addInsideId: id }));
    } else setData(recursiveTreeWalker({ items }));
    updateHeight();
  };

  const Row = ({
    data: dataGetter,
    index,
    rowStyle: style,
    rowComponent: RowComponent,
  }: {
    data: (index: number) => {
      row:
        | Readonly<{
            id: string;
            isLeaf: boolean;
            name: string;
            nestingLevel: number;
            padding: number;
            path: string[];
          }>
        | undefined;
    };
    index: number;
    rowStyle: any;
    rowComponent: React.FC<any>;
  }) => {
    const item = dataGetter(index);

    const dimensionCallback = useCallback(
      (rowRef) => {
        const key = `${index}`;
        const scrollbarWidth = scrollableElement?.offsetWidth - scrollableElement?.clientWidth || 0;
        const itemWidth = rowRef.scrollWidth + scrollbarWidth + 5;
        const itemHeight = rowRef.scrollHeight;

        if (width < itemWidth) {
          if (maxWidth < itemWidth) {
            heightAccumulator[key] = itemHeight;
            setWidth(maxWidth);
          } else {
            heightAccumulator[key] = rowHeight;
            setWidth(itemWidth);
          }
        } else heightAccumulator[key] = rowHeight;
        updateHeight();
      },
      [width],
    );

    return <RowComponent {...{ isEditable, item, style, dimensionCallback, maxWidth }} />;
  };

  const recursiveTreeWalker = ({
    items,
    depth,
    toggleItem,
    addInsideId,
  }: {
    items: RowItem[];
    depth?: number;
    toggleItem?: { [key: string]: number };
    addInsideId?: string;
  }) => {
    const stack: ExtendedData[] = [];

    for (let i = 0; i < items.length; i++) {
      const { children, label } = items[i];
      const definedDepth = depth || 0;
      const id = `${label}-${definedDepth}`;
      const addInside = addInsideId === id;
      const isOpen = (toggleItem && toggleItem[id]) || openNodes[id] || addInside || (defaultExpanded ? 1 : 2);

      const transformedData: ExtendedData = transformationCallback({
        node: items[i],
        nestingLevel: definedDepth,
        isFiltering: flatten,
        isLeaf: !children,
        childCount: children && countChildNodes(children),
        isOpen: isOpen === 1,
      });

      addInside && setOpenNodes({ ...openNodes, [id]: 1 });

      if ((children && isOpen === 1) || addInside || flatten) {
        stack.push({ ...transformedData });
        addInside &&
          stack.push(
            ...recursiveTreeWalker({ items: [blankItem(items[i].path, definedDepth + 1)], depth: definedDepth + 1 }),
          );
        children &&
          stack.push(...recursiveTreeWalker({ items: children, depth: definedDepth + 1, toggleItem, addInsideId }));
      } else stack.push({ ...transformedData });
    }
    return stack;
  };

  useEffect(() => {
    setData(recursiveTreeWalker({ items }));
  }, [items]);

  useEffect(() => {
    if (data?.length === 0) updateHeight();
  }, [data]);

  return (
    <div ref={containerRef}>
      <VariableSizeList
        ref={listRef}
        height={containerHeight + 4}
        itemCount={data?.length || 0}
        itemSize={rowHeightCalc}
        width={width}
        itemData={(index: number) => ({ row: data && data[index], toggle, addInside })}
      >
        {({ data, index, style }) => <Row data={data} rowStyle={style} index={index} rowComponent={rowComponent} />}
      </VariableSizeList>
    </div>
  );
};

export default TreeStructure;
